/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.solr.search;

import java.util.Locale;

/**
 * Simple class to help with parsing a string. <b>Note: This API is experimental and may change in
 * non backward-compatible ways in the future</b>
 */
public class StrParser {
  public String val;
  public int pos;
  public int end;

  public StrParser(String val) {
    this(val, 0, val.length());
  }

  public StrParser(String val, int start, int end) {
    this.val = val;
    this.pos = start;
    this.end = end;
  }

  public void eatws() {
    while (pos < end && Character.isWhitespace(val.charAt(pos))) pos++;
  }

  public char ch() {
    return pos < end ? val.charAt(pos) : 0;
  }

  public void skip(int nChars) {
    pos = Math.max(pos + nChars, end);
  }

  public boolean opt(String s) {
    eatws();
    int slen = s.length();
    if (val.regionMatches(pos, s, 0, slen)) {
      pos += slen;
      return true;
    }
    return false;
  }

  public boolean opt(char ch) {
    eatws();
    if (pos < end && val.charAt(pos) == ch) {
      pos++;
      return true;
    }
    return false;
  }

  public void expect(String s) throws SyntaxError {
    eatws();
    int slen = s.length();
    if (val.regionMatches(pos, s, 0, slen)) {
      pos += slen;
    } else {
      throw new SyntaxError("Expected '" + s + "' at position " + pos + " in '" + val + "'");
    }
  }

  public float getFloat() {
    eatws();
    char[] arr = new char[end - pos];
    int i;
    for (i = 0; i < arr.length; i++) {
      char ch = val.charAt(pos);
      if ((ch >= '0' && ch <= '9')
          || ch == '+'
          || ch == '-'
          || ch == '.'
          || ch == 'e'
          || ch == 'E') {
        pos++;
        arr[i] = ch;
      } else {
        break;
      }
    }

    return Float.parseFloat(new String(arr, 0, i));
  }

  public Number getNumber() {
    eatws();
    int start = pos;
    boolean flt = false;

    while (pos < end) {
      char ch = val.charAt(pos);
      if ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-') {
        pos++;
      } else if (ch == '.' || ch == 'e' || ch == 'E') {
        flt = true;
        pos++;
      } else {
        break;
      }
    }

    String v = val.substring(start, pos);
    if (flt) {
      return Double.parseDouble(v);
    } else {
      return Long.parseLong(v);
    }
  }

  public double getDouble() {
    eatws();
    char[] arr = new char[end - pos];
    int i;
    for (i = 0; i < arr.length; i++) {
      char ch = val.charAt(pos);
      if ((ch >= '0' && ch <= '9')
          || ch == '+'
          || ch == '-'
          || ch == '.'
          || ch == 'e'
          || ch == 'E') {
        pos++;
        arr[i] = ch;
      } else {
        break;
      }
    }

    return Double.parseDouble(new String(arr, 0, i));
  }

  public int getInt() {
    eatws();
    char[] arr = new char[end - pos];
    int i;
    for (i = 0; i < arr.length; i++) {
      char ch = val.charAt(pos);
      if ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-') {
        pos++;
        arr[i] = ch;
      } else {
        break;
      }
    }

    return Integer.parseInt(new String(arr, 0, i));
  }

  public byte getByte() {
    eatws();
    char[] arr = new char[end - pos];
    int i;
    for (i = 0; i < arr.length; i++) {
      char ch = val.charAt(pos);
      if ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-') {
        pos++;
        arr[i] = ch;
      } else {
        break;
      }
    }

    return Byte.parseByte(new String(arr, 0, i));
  }

  public String getId() throws SyntaxError {
    return getId("Expected identifier");
  }

  public String getId(String errMessage) throws SyntaxError {
    eatws();
    int id_start = pos;
    char ch;
    if (pos < end && (ch = val.charAt(pos)) != '$' && Character.isJavaIdentifierStart(ch)) {
      pos++;
      while (pos < end) {
        ch = val.charAt(pos);
        //          if (!Character.isJavaIdentifierPart(ch) && ch != '.' && ch != ':') {
        if (!Character.isJavaIdentifierPart(ch) && ch != '.') {
          break;
        }
        pos++;
      }
      return val.substring(id_start, pos);
    }

    if (errMessage != null) {
      throw new SyntaxError(errMessage + " at pos " + pos + " str='" + val + "'");
    }
    return null;
  }

  public String getGlobbedId(String errMessage) throws SyntaxError {
    eatws();
    int id_start = pos;
    char ch;
    if (pos < end
        && (ch = val.charAt(pos)) != '$'
        && (Character.isJavaIdentifierStart(ch) || ch == '?' || ch == '*')) {
      pos++;
      while (pos < end) {
        ch = val.charAt(pos);
        if (!(Character.isJavaIdentifierPart(ch) || ch == '?' || ch == '*') && ch != '.') {
          break;
        }
        pos++;
      }
      return val.substring(id_start, pos);
    }

    if (errMessage != null) {
      throw new SyntaxError(errMessage + " at pos " + pos + " str='" + val + "'");
    }
    return null;
  }

  /**
   * Skips leading whitespace and returns whatever sequence of non whitespace it can find (or hte
   * empty string)
   */
  public String getSimpleString() {
    eatws();
    int startPos = pos;
    char ch;
    while (pos < end) {
      ch = val.charAt(pos);
      if (Character.isWhitespace(ch)) break;
      pos++;
    }
    return val.substring(startPos, pos);
  }

  /**
   * Sort direction or null if current position does not indicate a sort direction. (True is desc,
   * False is asc). Position is advanced to after the comma (or end) when result is non null
   */
  public Boolean getSortDirection() throws SyntaxError {
    final int startPos = pos;
    final String order = getId(null);

    Boolean top = null;

    if (null != order) {
      final String orderLowerCase = order.toLowerCase(Locale.ROOT);
      if ("desc".equals(orderLowerCase) || "top".equals(orderLowerCase)) {
        top = true;
      } else if ("asc".equals(orderLowerCase) || "bottom".equals(orderLowerCase)) {
        top = false;
      }

      // it's not a legal direction if more stuff comes after it
      eatws();
      final char c = ch();
      if (0 == c) {
        // :NOOP
      } else if (',' == c) {
        pos++;
      } else {
        top = null;
      }
    }

    if (null == top) pos = startPos; // no direction, reset
    return top;
  }

  // return null if not a string
  public String getQuotedString() throws SyntaxError {
    eatws();
    char delim = peekChar();
    if (!(delim == '\"' || delim == '\'')) {
      return null;
    }
    int val_start = ++pos;
    StringBuilder sb = new StringBuilder(); // needed for escaping
    for (; ; ) {
      if (pos >= end) {
        throw new SyntaxError(
            "Missing end quote for string at pos " + (val_start - 1) + " str='" + val + "'");
      }
      char ch = val.charAt(pos);
      if (ch == '\\') {
        pos++;
        if (pos >= end) break;
        ch = val.charAt(pos);
        switch (ch) {
          case 'n':
            ch = '\n';
            break;
          case 't':
            ch = '\t';
            break;
          case 'r':
            ch = '\r';
            break;
          case 'b':
            ch = '\b';
            break;
          case 'f':
            ch = '\f';
            break;
          case 'u':
            if (pos + 4 >= end) {
              throw new SyntaxError(
                  "bad unicode escape \\uxxxx at pos" + (val_start - 1) + " str='" + val + "'");
            }
            ch = (char) Integer.parseInt(val.substring(pos + 1, pos + 5), 16);
            pos += 4;
            break;
        }
      } else if (ch == delim) {
        pos++; // skip over the quote
        break;
      }
      sb.append(ch);
      pos++;
    }

    return sb.toString();
  }

  // next non-whitespace char
  public char peek() {
    eatws();
    return pos < end ? val.charAt(pos) : 0;
  }

  // next char
  public char peekChar() {
    return pos < end ? val.charAt(pos) : 0;
  }

  @Override
  public String toString() {
    return "'" + val + "'" + ", pos=" + pos;
  }
}
